﻿/**
 * @copyright Copyright 2009 Structured Solutions
 * @license http://www.codeplex.com/ShippingAgent/license Microsoft Public License (Ms-PL)
 * @version 3.6
 */

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using BVSoftware.Bvc5.Core;
using BVSoftware.Bvc5.Core.BusinessRules;
using BVSoftware.Bvc5.Core.Catalog;
using BVSoftware.Bvc5.Core.Contacts;
using BVSoftware.Bvc5.Core.Content;
using BVSoftware.Bvc5.Core.Datalayer;
using BVSoftware.Bvc5.Core.Membership;
using BVSoftware.Bvc5.Core.Orders;
using BVSoftware.Bvc5.Core.Shipping;
using StructuredSolutions.ShippingAgent.Properties;

namespace StructuredSolutions.ShippingAgent
{
    [WebService(Namespace = "http://www.codeplex.com/ShippingAgent",
        Description = "Shipping Agent for BVC5 SP3")]
    public class ShippingAgentImpl : ShippingAgentService
    {
        private const string VERSION = "3.6.0";

        #region WebMethod Implementations

        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve the features supported by this implementation of Shipping Agent.")]
        public override Features GetFeatures()
        {
            try
            {
                AuthenticateUser();

                return new Features { SupportsNewAndUpdatedOrders = true };
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve features.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Return orders created or updated since pageKey. If pageKey is null or an empty
        /// string, then return all orders from today.
        /// </summary>
        /// <param name="orderStatusList">A list of order statuses to include.</param>
        /// <param name="paymentStatusList">A list of order payment statuses to include.</param>
        /// <param name="pageKey">The last order update date/time. May be
        /// null or an empty string if this is the first time this method is called.</param>
        /// <param name="maxCount">The maximum number of orders to return.</param>
        /// <returns>Up to maxCount OrderResults, sorted by order number; or an empty list or null 
        /// if there are no new orders.</returns>
        /// <remarks>
        /// <para>If maxCount orders are returned, the Shipper client will call this method again with the 
        /// same ranges and with lastOrderNumber set to the last order number in the previous result. In this
        /// case, use lastOrderNumber to calculate an appropriate new startOrderNumber (for example,
        /// startOrderNumber = lastOrderNumber + 1).</para>
        /// </remarks>
        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve orders placed since lastOrderNumber.")]
        public override OrderPage GetNewAndUpdatedOrders(string orderStatusList, string paymentStatusList, string pageKey, int maxCount)
        {
            try
            {
                AuthenticateUser();

                return GetNewAndUpdatedOrdersFromStore(orderStatusList, paymentStatusList, pageKey, maxCount);
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve new orders.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Return orders placed since pageKey. If pageKey is null or an empty
        /// string, then return all orders from today.
        /// </summary>
        /// <param name="orderStatusList">A list of order statuses to include.</param>
        /// <param name="paymentStatusList">A list of order payment statuses to include.</param>
        /// <param name="pageKey">The last order number in the previous results. May be
        /// null or an empty string if this is the first time this method is called.</param>
        /// <param name="maxCount">The maximum number of orders to return.</param>
        /// <returns>Up to maxCount OrderResults, sorted by order number; or an empty list or null 
        /// if there are no new orders.</returns>
        /// <remarks>
        /// <para>If maxCount orders are returned, the Shipper client will call this method again with the 
        /// same ranges and with lastOrderNumber set to the last order number in the previous result. In this
        /// case, use lastOrderNumber to calculate an appropriate new startOrderNumber (for example,
        /// startOrderNumber = lastOrderNumber + 1).</para>
        /// </remarks>
        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve orders placed since lastOrderNumber.")]
        public override OrderPage GetNewOrders(string orderStatusList, string paymentStatusList, string pageKey, int maxCount)
        {
            try
            {
                AuthenticateUser();

                return GetNewOrdersFromStore(orderStatusList, paymentStatusList, pageKey, maxCount);
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve new orders.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Return one order.
        /// </summary>
        /// <param name="orderId">The ID of the order.</param>
        /// <returns>The OrderResult.</returns>
        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve one order.")]
        public override OrderResult GetOrder(string orderId)
        {
            try
            {
                AuthenticateUser();

                if (string.IsNullOrEmpty(orderId))
                    throw new ApplicationException("Invalid order ID.");

                return GetOrderResult(orderId);
            }
            catch (ApplicationException ex)
            {
                var result = new OrderResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                var result = new OrderResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        /// <summary>
        /// Return all the orders that fall on and within the specified ranges.
        /// </summary>
        /// <param name="startOrderNumber">The starting order number; or null or an empty string to start at the first order number in the store.</param>
        /// <param name="endOrderNumber">The ending order number; or null or an empty string to end with the last order number in the store.</param>
        /// <param name="startOrderDate">The starting order date. If no start date is specified, start from the earliest order.</param>
        /// <param name="startOrderDateSpecified">True if startOrderDate is specified, false otherwise.</param>
        /// <param name="endOrderDate">The ending order date. If no end date is specified, end with the most recent order.</param>
        /// <param name="endOrderDateSpecified">True if endOrderDate is specified, false otherwise.</param>
        /// <param name="orderStatusList">A list of order statuses to include.</param>
        /// <param name="paymentStatusList">A list of order payment statuses to include.</param>
        /// <param name="pageKey">The last order number from the previous result; or null or an empty string to ignore this parameter.</param>
        /// <param name="maxCount">The maximum number of orders to return.</param>
        /// <returns>Up to maxCount Order's, sorted by order number; or an empty list or null 
        /// if there are no orders that fall on and within the specified ranges.</returns>
        /// <remarks>If maxCount orders are returned, the Shipper client will call this method again with the 
        /// same ranges and with lastOrderNumber set to the last order number in the previous result. In this
        /// case, use lastOrderNumber to calculate an appropriate new startOrderNumber (for example,
        /// startOrderNumber = lastOrderNumber + 1).
        /// </remarks>
        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Return the list of orders that fall on and within the specified ranges.")]
        public override OrderPage GetOrdersByRange(string startOrderNumber, string endOrderNumber, DateTime startOrderDate, bool startOrderDateSpecified, DateTime endOrderDate, bool endOrderDateSpecified, string orderStatusList, string paymentStatusList, string pageKey, int maxCount)
        {
            try
            {
                AuthenticateUser();

                var nullableStartDate = ShippingAgentHelper.ConvertWebServiceNullable(startOrderDate, startOrderDateSpecified);
                var nullableEndDate = ShippingAgentHelper.ConvertWebServiceNullable(endOrderDate, endOrderDateSpecified);
                return GetOrdersByRangeFromStore(startOrderNumber, endOrderNumber, nullableStartDate, nullableEndDate, orderStatusList, paymentStatusList, pageKey, maxCount);
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve orders by range.", SoapException.ServerFaultCode, ex);
            }
        }

        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve the list of all searchable order statuses.")]
        public override OrderStatus[] GetOrderStatuses()
        {
            try
            {
                AuthenticateUser();

                var statuses = new List<OrderStatus>();
                foreach (var bvStatusCode in OrderStatusCode.FindAll())
                {
                    statuses.Add(
                        new OrderStatus
                            {
                                Name = bvStatusCode.StatusName,
                                OrderStatusId = bvStatusCode.Bvin
                            });
                }
                return statuses.ToArray();
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve order statuses.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Return one specific package.
        /// </summary>
        /// <param name="orderId">The ID of the order.</param>
        /// <param name="packageId">The ID of the package.</param>
        /// <returns>A PackageResult.</returns>
        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve one package.")]
        public override PackageResult GetPackage(string orderId, string packageId)
        {
            try
            {
                AuthenticateUser();

                if (string.IsNullOrEmpty(orderId))
                    throw new ApplicationException("Invalid order ID.");
                if (string.IsNullOrEmpty(packageId))
                    throw new ApplicationException("Invalid package ID.");

                return GetPackageResult(orderId, packageId);
            }
            catch (ApplicationException ex)
            {
                var result = new PackageResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                var result = new PackageResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve the list of all searchable order payment statuses.")]
        public override PaymentStatus[] GetPaymentStatuses()
        {
            try
            {
                AuthenticateUser();

                var statuses = new List<PaymentStatus>();
                foreach (OrderPaymentStatus bvPaymentStatus in Enum.GetValues(typeof(OrderPaymentStatus)))
                {
                    statuses.Add(
                        new PaymentStatus
                            {
                                Name = ShippingAgentHelper.SplitCompoundName(bvPaymentStatus.ToString()),
                                PaymentStatusId = ((int) bvPaymentStatus).ToString()
                            });
                }
                return statuses.ToArray();
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve payment statuses.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Returns the complete list of shipping rate providers offered by the store.
        /// </summary>
        /// <returns>The list of RateProviders's.</returns>
        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve the list of shipping rate providers.")]
        public override RateProvider[] GetRateProviders()
        {
            try
            {
                AuthenticateUser();

                return GetRateProvidersFromStore().ToArray();
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve rate providers.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Returns the complete list of tracking providers supported by the store. The
        /// Shipper client may assign any tracking provider to a package, regardless of
        /// how the shipping cost was initially calculated.
        /// </summary>
        /// <returns>The list of TrackingProvider's.</returns>
        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve the list of package tracking providers.")]
        public override TrackingProvider[] GetTrackingProviders()
        {
            try
            {
                AuthenticateUser();

                return GetTrackingProvidersFromStore().ToArray();
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve tracking providers.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Return the ShippingAgent.wsdl implemented by this ShippingAgent.
        /// </summary>
        /// <returns>A string value that represents the version using the format
        /// Major.Minor.Revision. For example, "3.0.0".</returns>
        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Retrieve the WSDL interface version implemented by this Shipping Agent.")]
        public override string GetVersion()
        {
            try
            {
                AuthenticateUser();

                return VERSION;
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve version.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Record package information.
        /// </summary>
        /// <param name="update">The package information to record.</param>
        /// <returns>If the update is successful, the UpdateResult contains a fresh
        /// copy of the Order that contains the package. If the update is not
        /// successful, the UpdateResult contains the reason why the update failed.</returns>
        /// <remarks>The Shipper client may not include all Update elements. null
        /// elements should be ignored. If the ShipDate is included, then the package
        /// should be marked shipped.</remarks>
        [SoapHeader("CredentialsValue", Direction = SoapHeaderDirection.In)]
        [WebMethod(Description = "Record package information.")]
        public override UpdateResult UpdatePackage(Update update)
        {
            try
            {
                AuthenticateUser();

                if (update == null)
                    return null;
                if (string.IsNullOrEmpty(update.OrderId))
                    throw new ApplicationException("Invalid order ID.");
                if (string.IsNullOrEmpty(update.PackageId))
                    throw new ApplicationException("Invalid package ID.");
                return UpdatePackageInStore(update);
            }
            catch (ApplicationException ex)
            {
                var result = new UpdateResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                var result = new UpdateResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        #endregion

        #region Platform Specific Methods

        private const string NULL_PROVIDER_ID = "0567206B-9AFA-4063-BF6F-B8C096706E92";
        private const string ORDER_DISCOUNT_ID = "OrderDiscount";
        private const string SHIPPING_DISCOUNT_ID = "ShippingDiscount";

        protected static OrderPage GetNewAndUpdatedOrdersFromStore(string orderStatusList, string paymentStatusList, string pageKey, int maxCount)
        {
            if (paymentStatusList == null) throw new ArgumentNullException("paymentStatusList");
            var parameters = new List<DataRequestParameter>();
            var query = BuildOrderQuery(parameters, orderStatusList, paymentStatusList);
            query.Append(" AND [LastUpdated] > @LastUpdated ");
            if (string.IsNullOrEmpty(pageKey))
            {
                parameters.Add(new DataRequestParameter("@LastUpdated", DateTime.Today));
            }
            else
            {
                long binaryDate;
                if (!long.TryParse(pageKey, out binaryDate))
                    throw new ApplicationException(string.Format("Invalid paging key '{0}'.", pageKey));

                parameters.Add(new DataRequestParameter("@LastUpdated", DateTime.FromBinary(binaryDate)));
            }

            var request = new DataRequest();
            request.Command = "SELECT COUNT(*) AS [total] " + query;
            request.CommandType = CommandType.Text;
            foreach (var parameter in parameters)
            {
                request.Parameters.Add(parameter);
            }

            var total = SqlDataHelper.ExecuteAndReadInteger(request, "total");
            if (total <= 0)
                return null;

            request = new DataRequest();
            request.Command = "SELECT TOP " + maxCount + " [bvin], [LastUpdated] " + query + " ORDER BY [LastUpdated]";
            request.CommandType = CommandType.Text;
            foreach (var parameter in parameters)
            {
                request.Parameters.Add(parameter);
            }

            var orderIds = new List<string>();
            var lastUpdated = DateTime.UtcNow;
            using (var reader = SqlDataHelper.ExecuteReader(request))
            {
                while (reader.Read())
                {
                    orderIds.Add(reader.GetString(0));
                    if (!reader.IsDBNull(1)) lastUpdated = reader.GetDateTime(1);
                }
            }

            if (orderIds.Count == 0)
                return null;

            var result = new OrderPage();
            result.Key = lastUpdated.ToBinary().ToString();
            result.Orders = orderIds.ToArray();
            result.Remaining = orderIds.Count < maxCount ? 0 : total - orderIds.Count;
            return result;
        }

        protected static OrderPage GetNewOrdersFromStore(string orderStatusList, string paymentStatusList, string pageKey, int maxCount)
        {
            var parameters = new List<DataRequestParameter>();
            var query = BuildOrderQuery(parameters, orderStatusList, paymentStatusList);
            if (string.IsNullOrEmpty(pageKey))
            {
                query.Append(" AND [TimeOfOrder] > @TimeOfOrder ");
                parameters.Add(new DataRequestParameter("@TimeOfOrder", DateTime.Today));
            }
            else
            {
                query.Append(" AND [OrderNumber] > @OrderNumber ");
                parameters.Add(new DataRequestParameter("@OrderNumber", pageKey));
            }

            var request = new DataRequest();
            request.Command = "SELECT COUNT(*) AS [total] " + query;
            request.CommandType = CommandType.Text;
            foreach (var parameter in parameters)
            {
                request.Parameters.Add(parameter);
            }

            var total = SqlDataHelper.ExecuteAndReadInteger(request, "total");
            if (total <= 0)
                return null;

            request = new DataRequest();
            request.Command = "SELECT TOP " + maxCount + " [bvin], [OrderNumber] " + query + " ORDER BY [OrderNumber]";
            request.CommandType = CommandType.Text;
            foreach (var parameter in parameters)
            {
                request.Parameters.Add(parameter);
            }

            var orderIds = new List<string>();
            var lastOrderNumber = string.Empty;
            using (var reader = SqlDataHelper.ExecuteReader(request))
            {
                while (reader.Read())
                {
                    orderIds.Add(reader.GetString(0));
                    if (!reader.IsDBNull(1)) lastOrderNumber = reader.GetString(1);
                }
            }

            if (orderIds.Count == 0)
                return null;

            var result = new OrderPage();
            result.Key = lastOrderNumber;
            result.Orders = orderIds.ToArray();
            result.Remaining = orderIds.Count < maxCount ? 0 : total - orderIds.Count;
            return result;
        }

        protected static OrderResult GetOrderResult(string orderId)
        {
            var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(orderId);
            if (bvOrder == null || !bvOrder.Bvin.Equals(orderId, StringComparison.InvariantCultureIgnoreCase))
                throw new ApplicationException("Invalid order ID.");

            return BuildOrderResult(bvOrder);
        }

        protected static OrderPage GetOrdersByRangeFromStore(string startOrderNumber, string endOrderNumber, DateTime? startOrderDate,
                                                             DateTime? endOrderDate, string orderStatusList, string paymentStatusList, string pageKey, int maxCount)
        {
            var parameters = new List<DataRequestParameter>();
            var query = BuildOrderQuery(parameters, orderStatusList, paymentStatusList);
            if (startOrderDate.HasValue)
            {
                query.Append(" AND [TimeOfOrder] >= @StartOrderDate ");
                parameters.Add(new DataRequestParameter("@StartOrderDate", startOrderDate.Value));
            }
            if (endOrderDate.HasValue)
            {
                query.Append(" AND [TimeOfOrder] <= @EndOrderDate ");
                parameters.Add(new DataRequestParameter("@EndOrderDate", endOrderDate.Value));
            }
            if (!string.IsNullOrEmpty(startOrderNumber))
            {
                query.Append(" AND [OrderNumber] >= @StartOrderNumber ");
                parameters.Add(new DataRequestParameter("@StartOrderNumber", startOrderNumber));
            }
            if (!string.IsNullOrEmpty(endOrderNumber))
            {
                query.Append(" AND [OrderNumber] <= @EndOrderNumber ");
                parameters.Add(new DataRequestParameter("@EndOrderNumber", endOrderNumber));
            }
            if (!string.IsNullOrEmpty(pageKey))
            {
                query.Append(" AND [OrderNumber] > @PageKey ");
                parameters.Add(new DataRequestParameter("@PageKey", pageKey));
            }

            var request = new DataRequest();
            request.Command = "SELECT COUNT(*) AS [total] " + query;
            request.CommandType = CommandType.Text;
            foreach (var parameter in parameters)
            {
                request.Parameters.Add(parameter);
            }

            var total = SqlDataHelper.ExecuteAndReadInteger(request, "total");
            if (total <= 0)
                return null;

            request = new DataRequest();
            request.Command = "SELECT TOP " + maxCount + " [bvin], [OrderNumber] " + query + " ORDER BY [OrderNumber]";
            request.CommandType = CommandType.Text;
            foreach (var parameter in parameters)
            {
                request.Parameters.Add(parameter);
            }

            var orderIds = new List<string>();
            var lastOrderNumber = string.Empty;
            using (var reader = SqlDataHelper.ExecuteReader(request))
            {
                while (reader.Read())
                {
                    orderIds.Add(reader.GetString(0));
                    if (!reader.IsDBNull(1)) lastOrderNumber = reader.GetString(1);
                }
            }

            if (orderIds.Count == 0)
                return null;

            var result = new OrderPage();
            result.Key = lastOrderNumber;
            result.Orders = orderIds.ToArray();
            result.Remaining = orderIds.Count < maxCount ? 0 : total - orderIds.Count;
            return result;
        }

        protected static List<TrackingProvider> GetTrackingProvidersFromStore()
        {
            var trackingProviders = new List<TrackingProvider>();
            foreach (var bvProvider in AvailableProviders.Providers)
            {
                if (!bvProvider.SupportsTracking) continue;

                TrackingProvider provider = new TrackingProvider();
                provider.Name = bvProvider.Name;
                provider.TrackingProviderId = bvProvider.ProviderId;
                trackingProviders.Add(provider);
            }
            return trackingProviders;
        }

        protected UpdateResult UpdatePackageInStore(Update update)
        {
            var bvPackage = BVSoftware.Bvc5.Core.Shipping.Package.FindByBvin(update.PackageId);
            if (bvPackage == null || !bvPackage.Bvin.Equals(update.PackageId, StringComparison.InvariantCultureIgnoreCase))
                throw new ApplicationException("Invalid package ID.");

            return BuildUpdateResult(update, bvPackage);
        }

        #endregion

        private static bool AddressesAreEqual(BVSoftware.Bvc5.Core.Contacts.Address bvAddress, ShippingAddress shippingAddress, string shippingCountryId,
                                              string shippingRegionId)
        {
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.City, shippingAddress.City, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Company, shippingAddress.Company, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.CountryBvin, shippingCountryId, StringComparison.InvariantCultureIgnoreCase)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Fax, shippingAddress.Fax, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.FirstName, shippingAddress.FirstName, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.LastName, shippingAddress.LastName, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Line1, shippingAddress.Line1, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Line2, shippingAddress.Line2, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Phone, shippingAddress.Phone, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.PostalCode, shippingAddress.PostalCode, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.RegionBvin, shippingRegionId, StringComparison.InvariantCultureIgnoreCase)) return false;
            return bvAddress.Residential == shippingAddress.Residential;
        }

        private void AuthenticateUser()
        {
            if (CredentialsValue == null
                || string.IsNullOrEmpty(CredentialsValue.Username)
                || string.IsNullOrEmpty(CredentialsValue.Password))
            {
                throw new ApplicationException("You must supply a valid username and password.");
            }

            // 
            // Validate the user credentials 
            // 
            var validation = UserAccount.ValidateUser(CredentialsValue.Username, CredentialsValue.Password);
            if (!validation.Success)
            {
                throw new ApplicationException(validation.Message);
            }

            var userAccount = UserAccount.FindByUserName(CredentialsValue.Username);
            if (userAccount == null || string.IsNullOrEmpty(userAccount.Bvin))
            {
                throw new ApplicationException("You must supply a valid username and password");
            }

            if (!UserAccount.DoesUserHavePermission(userAccount.Bvin, SystemPermissions.LoginToAdmin))
            {
                throw new ApplicationException("The username and password are not authorized to access Shipping Agent.");
            }
        }

        private static Address BuildAddress(BVSoftware.Bvc5.Core.Contacts.Address bvAddress, string email)
        {
            if (bvAddress == null) return null;

            Address address = new Address();
            address.AddressId = string.IsNullOrEmpty(bvAddress.Bvin) ? GetUniqueAddressId(bvAddress) : bvAddress.Bvin;
            address.City = ShippingAgentHelper.StringOrNull(bvAddress.City);
            address.Company = ShippingAgentHelper.StringOrNull(bvAddress.Company);
            address.Country = ShippingAgentHelper.StringOrNull(bvAddress.CountryName);
            if (!string.IsNullOrEmpty(bvAddress.CountryBvin))
            {
                var country = Country.FindByBvin(bvAddress.CountryBvin);
                if (country != null && country.Bvin.Equals(bvAddress.CountryBvin, StringComparison.InvariantCultureIgnoreCase))
                    address.Country = country.IsoCode;
            }
            address.Email = ShippingAgentHelper.StringOrNull(email);
            address.Fax = ShippingAgentHelper.StringOrNull(bvAddress.Fax);
            address.FirstName = ShippingAgentHelper.StringOrNull(bvAddress.FirstName);
            address.LastName = ShippingAgentHelper.StringOrNull(bvAddress.LastName);
            address.Line1 = ShippingAgentHelper.StringOrNull(bvAddress.Line1);
            address.Line2 = ShippingAgentHelper.StringOrNull(bvAddress.Line2);
            address.Phone = ShippingAgentHelper.StringOrNull(bvAddress.Phone);
            address.PostalCode = ShippingAgentHelper.StringOrNull(bvAddress.PostalCode);
            address.Region = ShippingAgentHelper.StringOrNull(bvAddress.RegionName);

            return address;
        }

        private static NamedAmount[] BuildCharges(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            const string HANDLING = "Handling";
            const string SHIPPING = "Shipping";
            const string TAXES = "Taxes";

            var charges = new NamedAmountList();
            if (bvOrder.HandlingTotal != 0) charges[HANDLING].Amount += bvOrder.HandlingTotal;
            if (bvOrder.ShippingTotal != 0) charges[SHIPPING].Amount += bvOrder.ShippingTotal;
            if (bvOrder.TaxTotal != 0) charges[TAXES].Amount += bvOrder.TaxTotal;
            if (bvOrder.TaxTotal2 != 0) charges[TAXES].Amount += bvOrder.TaxTotal2;
            return charges.Count > 0 ? charges.ToArray() : null;
        }

        private static Customer BuildCustomer(UserAccount user)
        {
            if (user == null) return null;

            Customer customer = new Customer();
            customer.CustomerId = user.Bvin;
            customer.CustomerUrl = BuildCustomerUrl("MyAccount_Orders.aspx", "");
            customer.Email = ShippingAgentHelper.StringOrNull(user.Email);
            customer.FirstName = ShippingAgentHelper.StringOrNull(user.FirstName);
            customer.LastName = ShippingAgentHelper.StringOrNull(user.LastName);
            customer.MerchantUrl = BuildMerchantUrl("BVAdmin/People/users_edit.aspx", "id=" + user.Bvin);
            customer.UserName = user.UserName;
            return customer;
        }

        private static string BuildCustomerUrl(string path, string query)
        {
            try
            {
                UriBuilder uri = new UriBuilder(WebAppSettings.SiteStandardRoot);
                uri.Path += path;
                uri.Query = query;
                return uri.Uri.ToString();
            }
            catch
            {
                return string.Empty;
            }
        }

        private static decimal BuildInsuranceValue(IEnumerable<PackageItem> bvPackageItems)
        {
            decimal value = 0;

            foreach (var bvPackageItem in bvPackageItems)
            {
                if (bvPackageItem.Quantity <= 0)
                    continue;

                var bvItem = LineItem.FindByBvin(bvPackageItem.LineItemBvin);
                if (bvItem == null) continue;
                if (bvItem.AssociatedProduct == null) continue;

                if (bvItem.AssociatedProduct.ShippingMode != ShippingMode.None)
                {
                    switch (Settings.Default.InsuranceValue.Replace(" ", string.Empty).ToUpperInvariant())
                    {
                        case "SITECOST":
                            if (bvItem.AssociatedProduct != null)
                            {
                                value += bvPackageItem.Quantity * bvItem.AssociatedProduct.SiteCost;
                            }
                            break;
                        default:
                            value += bvPackageItem.Quantity * bvItem.AdjustedPrice;
                            break;
                    }
                }
            }

            return value;
        }

        private static Property[] BuildItemProperties(LineItem bvLineItem)
        {
            var properties = new List<Property>();
            foreach (var bvInput in bvLineItem.Inputs)
            {
                if (!bvInput.DisplayToCustomer)
                    continue;

                var property = new Property();
                property.Name = bvInput.InputName;
                property.Value = bvInput.InputDisplayValue;
                properties.Add(property);
            }
            foreach (var bvLineItemModifier in bvLineItem.Modifiers)
            {
                if (!bvLineItemModifier.DisplayToCustomer)
                    continue;

                var bvModifier = ProductModifier.FindByBvin(bvLineItemModifier.ModifierBvin);
                if (bvModifier == null || !bvModifier.Bvin.Equals(bvLineItemModifier.ModifierBvin, StringComparison.InvariantCultureIgnoreCase))
                    continue;

                foreach (var bvModifierOption in bvModifier.ModifierOptions)
                {
                    if (!bvModifierOption.Bvin.Equals(bvLineItemModifier.ModifierValue, StringComparison.InvariantCultureIgnoreCase))
                        continue;

                    var property = new Property();
                    property.Name = bvLineItemModifier.ModifierName;
                    property.Value = bvModifierOption.DisplayText;
                    properties.Add(property);
                    break;
                }
            }
            return properties.Count == 0 ? null : properties.ToArray();
        }

        private static ItemResult BuildItemResult(LineItem bvLineItem, decimal quantity)
        {
            var itemResult = new ItemResult();
            var item = new Item();
            itemResult.Item = item;

            item.Code = bvLineItem.ProductSku;
            item.ItemId = bvLineItem.Bvin;
            item.LineTotal = bvLineItem.LineTotal;
            item.Name = bvLineItem.ProductName;
            item.Price = bvLineItem.AdjustedPrice;
            item.Quantity = Convert.ToDouble(quantity);

            try
            {
                item.Properties = BuildItemProperties(bvLineItem);
                item.Weight = Convert.ToDouble(quantity * bvLineItem.GetSingleItemWeight());

                if (bvLineItem.AssociatedProduct != null)
                {
                    item.Shippable = bvLineItem.AssociatedProduct.ShippingMode != ShippingMode.None;
                    if (bvLineItem.AssociatedProduct.TrackInventory)
                    {
                        var bvInventory = ProductInventory.FindByBvin(bvLineItem.ProductId);
                        item.Inventory = Convert.ToDouble(bvInventory.QuantityAvailable);
                    }
                    if (!string.IsNullOrEmpty(bvLineItem.AssociatedProduct.ManufacturerId))
                    {
                        var bvManufacturer = Manufacturer.FindByBvin(bvLineItem.AssociatedProduct.ManufacturerId);
                        if (bvManufacturer != null &&
                            bvManufacturer.Bvin.Equals(bvLineItem.AssociatedProduct.ManufacturerId, StringComparison.InvariantCultureIgnoreCase))
                        {
                            item.Manufacturer = new Supplier();
                            item.Manufacturer.Email = ShippingAgentHelper.StringOrNull(bvManufacturer.EmailAddress);
                            item.Manufacturer.Name = ShippingAgentHelper.StringOrNull(bvManufacturer.DisplayName);
                            item.Manufacturer.SupplierId = bvManufacturer.Bvin;
                        }
                    }
                    if (!string.IsNullOrEmpty(bvLineItem.AssociatedProduct.VendorId))
                    {
                        var bvVendor = Vendor.FindByBvin(bvLineItem.AssociatedProduct.VendorId);
                        if (bvVendor != null && bvVendor.Bvin.Equals(bvLineItem.AssociatedProduct.VendorId, StringComparison.InvariantCultureIgnoreCase))
                        {
                            item.Vendor = new Supplier();
                            item.Vendor.Email = ShippingAgentHelper.StringOrNull(bvVendor.EmailAddress);
                            item.Vendor.Name = ShippingAgentHelper.StringOrNull(bvVendor.DisplayName);
                            item.Vendor.SupplierId = bvVendor.Bvin;
                        }
                    }
                }
                itemResult.Success = true;
            }
            catch (ApplicationException ex)
            {
                itemResult.Message = ex.Message;
                itemResult.Success = false;
            }
            catch (Exception ex)
            {
                itemResult.Message = ex.ToString();
                itemResult.Success = false;
            }

            return itemResult;
        }

        private static string BuildMerchantUrl(string path, string query)
        {
            try
            {
                var uri = new UriBuilder(WebAppSettings.SiteSecureRoot);
                uri.Path += path;
                uri.Query = query;
                return uri.Uri.ToString();
            }
            catch
            {
                return string.Empty;
            }
        }

        private static ItemResult[] BuildOrderItemResults(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            var itemResults = new List<ItemResult>();

            foreach (var bvItem in bvOrder.Items)
            {
                itemResults.Add(BuildItemResult(bvItem, bvItem.Quantity));
            }

            // This is normally wasted code. BVC5 only tracks discounts until the
            // order is placed. So this order will always have OrderDiscounts = 0,
            // and ShippingDiscounts = 0.
            if (bvOrder.OrderDiscounts != 0)
            {
                itemResults.Add(new ItemResult
                {
                    Item = new Item
                    {
                        Code = "Discount",
                        ItemId = ORDER_DISCOUNT_ID,
                        LineTotal = bvOrder.OrderDiscounts,
                        Name = "Order Discount",
                        Price = bvOrder.OrderDiscounts,
                        Quantity = 1,
                        Shippable = false
                    },
                    Success = true
                });
            }

            if (bvOrder.ShippingDiscounts != 0)
            {
                itemResults.Add(new ItemResult
                {
                    Item = new Item
                    {
                        Code = "Discount",
                        ItemId = SHIPPING_DISCOUNT_ID,
                        LineTotal = bvOrder.ShippingDiscounts,
                        Name = "Shipping Discount",
                        Price = bvOrder.ShippingDiscounts,
                        Quantity = 1,
                        Shippable = false
                    },
                    Success = true
                });
            }

            return itemResults.Count > 0 ? itemResults.ToArray() : null;
        }

        private static StringBuilder BuildOrderQuery(ICollection<DataRequestParameter> parameters, string orderStatusList, string paymentStatusList)
        {
            var query = new StringBuilder(" FROM [bvc_Order] WHERE [IsPlaced] = 1 ");

            if (!string.IsNullOrEmpty(orderStatusList))
            {
                var statusCodes = orderStatusList.Split(new[] {',', ';'}, StringSplitOptions.RemoveEmptyEntries);
                if (statusCodes.Length > 0)
                {
                    var parameterNames = new List<string>();
                    for (int index = 0; index < statusCodes.Length; index++)
                    {
                        var parameterName = "@StatusCode" + index;
                        parameterNames.Add(parameterName);
                        parameters.Add(new DataRequestParameter(parameterName, statusCodes[index]));
                    }
                    query.AppendFormat(" AND [StatusCode] IN ({0})", string.Join(",", parameterNames.ToArray()));
                }
            }

            if (!string.IsNullOrEmpty(paymentStatusList))
            {
                query.AppendFormat(" AND [PaymentStatus] IN ({0})", paymentStatusList);
            }

            return query;
        }

        private static OrderResult BuildOrderResult(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            var order = new Order();

            // Build the "simple" fields that have little or no chance of failing
            order.CustomerUrl = BuildCustomerUrl("Myaccount_Orders_Details.aspx", "id=" + bvOrder.Bvin);
            order.MerchantUrl = BuildMerchantUrl("BVAdmin/Orders/ViewOrder.aspx", "id=" + bvOrder.Bvin);
            order.OrderDate = bvOrder.TimeOfOrder;
            order.LastUpdated = bvOrder.LastUpdated;
            order.OrderId = bvOrder.Bvin;
            order.OrderNumber = bvOrder.OrderNumber;
            order.OrderStatus = bvOrder.StatusName;
            order.OrderTotal = bvOrder.GrandTotal;
            order.PaymentStatus = ShippingAgentHelper.SplitCompoundName(bvOrder.PaymentStatus.ToString());
            order.ShippingStatus = BuildShippingStatus(bvOrder.ShippingStatus);

            try
            {
                order.BillingAddress = BuildAddress(bvOrder.BillingAddress, bvOrder.UserEmail);
                order.Charges = BuildCharges(bvOrder);
                order.Customer = BuildCustomer(bvOrder.User);
                order.Items = BuildOrderItemResults(bvOrder);
                order.Packages = BuildPackageRefs(bvOrder);
                order.Payments = BuildPayments(bvOrder);

                var text = string.Format(ShippingAgentHelper.ORDER_EXPORT_TEXT, HttpContext.Current == null ? "(no IP in request)" : HttpContext.Current.Request.UserHostAddress);
                bvOrder.AddNote(new OrderNote { Note = text, NoteType = OrderNoteType.System });

                return new OrderResult { Success = true, Order = order };
            }
            catch (ApplicationException ex)
            {
                return new OrderResult { Success = false, Message = ex.Message, Order = order };
            }
            catch (Exception ex)
            {
                return new OrderResult { Success = false, Message = ex.ToString(), Order = order };
            }
        }

        private static ItemResult[] BuildPackageItemResults(BVSoftware.Bvc5.Core.Shipping.Package bvPackage)
        {
            var itemResults = new List<ItemResult>();

            foreach (var bvPackageItem in bvPackage.Items)
            {
                if (bvPackageItem.Quantity <= 0)
                    continue;

                var bvItem = LineItem.FindByBvin(bvPackageItem.LineItemBvin);
                if (bvItem == null || !bvItem.Bvin.Equals(bvPackageItem.LineItemBvin, StringComparison.InvariantCultureIgnoreCase))
                    continue;

                itemResults.Add(BuildItemResult(bvItem, bvPackageItem.Quantity));
            }

            return itemResults.Count > 0 ? itemResults.ToArray() : null;
        }

        private static PackageRef[] BuildPackageRefs(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            var packageRefs = new List<PackageRef>();
            foreach (var bvPackage in GetPackages(bvOrder))
            {
                var packageRef = new PackageRef();
                packageRef.OrderId = bvOrder.Bvin;
                packageRef.PackageId = bvPackage.Bvin;
                packageRefs.Add(packageRef);
            }
            return packageRefs.Count > 0 ? packageRefs.ToArray() : null;
        }

        private static PackageResult BuildPackageResult(BVSoftware.Bvc5.Core.Shipping.Package bvPackage)
        {
            var package = new Package();
            package.CustomerUrl = BuildCustomerUrl("Myaccount_Orders_Details.aspx", "id=" + bvPackage.OrderId);
            package.MerchantUrl = BuildMerchantUrl("BVAdmin/Orders/ShipOrder.aspx", "id=" + bvPackage.OrderId);
            package.OrderId = bvPackage.OrderId;
            package.PackageId = bvPackage.Bvin;

            try
            {
                // Add missing dimensions
                if (!bvPackage.HasShipped && bvPackage.Height == 0 && bvPackage.Weight == 0 && bvPackage.Length == 0)
                {
                    var bvShippingGroup = new ShippingGroup();
                    foreach (var bvPackageItem in bvPackage.Items)
                    {
                        bvShippingGroup.Items.Add(LineItem.FindByBvin(bvPackageItem.LineItemBvin));
                    }
                    bvShippingGroup.GenerateDimensions();
                    bvPackage.Height = bvShippingGroup.Height;
                    bvPackage.Length = bvShippingGroup.Length;
                    bvPackage.Width = bvShippingGroup.Width;
                    if (bvPackage.Weight == 0)
                        bvPackage.Weight = bvShippingGroup.Weight;
                }

                var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvPackage.OrderId);
                package.Destination = BuildShippingAddress(bvOrder.ShippingAddress, bvOrder.UserEmail);
                package.Height = GetDimensionInInches(bvPackage.Height, bvPackage.SizeUnits);
                package.Instructions = ShippingAgentHelper.StringOrNull(bvOrder.Instructions);
                package.InsuranceValue = BuildInsuranceValue(bvPackage.Items);
                package.Items = BuildPackageItemResults(bvPackage);
                package.Length = GetDimensionInInches(bvPackage.Length, bvPackage.SizeUnits);
                package.RateOptionName = ShippingAgentHelper.StringOrNull(bvOrder.ShippingMethodDisplayName);
                if (string.IsNullOrEmpty(package.RateOptionName))
                    package.RateOptionName = "(unknown)";
                if (!string.IsNullOrEmpty(bvOrder.ShippingProviderId) && !bvOrder.ShippingProviderId.Equals(NULL_PROVIDER_ID, StringComparison.InvariantCultureIgnoreCase))
                {
                    var bvProvider = AvailableProviders.FindProviderById(bvOrder.ShippingProviderId);
                    package.RateProviderName = bvProvider.ProviderId.Equals(NULL_PROVIDER_ID, StringComparison.InvariantCultureIgnoreCase) ? "(unknown)" : ShippingAgentHelper.StringOrNull(bvProvider.Name);
                }
                if (bvPackage.HasShipped)
                {
                    package.ShipDate = bvPackage.ShipDate;
                    package.ShipDateSpecified = true;
                }
                else
                {
                    package.ShipDateSpecified = false;
                }
                package.Shipped = bvPackage.HasShipped;
                package.TrackingNumber = bvPackage.TrackingNumber;
                if (!string.IsNullOrEmpty(bvPackage.ShippingProviderId) && !bvPackage.ShippingProviderId.Equals(NULL_PROVIDER_ID, StringComparison.InvariantCultureIgnoreCase))
                {
                    // If the provider has been deleted, this will return the
                    // the null provider, which does not support tracking.
                    var bvProvider = AvailableProviders.FindProviderById(bvPackage.ShippingProviderId);
                    if (bvProvider.SupportsTracking)
                    {
                        package.TrackingProviderId = bvPackage.ShippingProviderId;
                        package.TrackingProviderName = bvProvider.Name;
                    }
                }

                var bvWarehouse = WebAppSettings.SiteShippingAddress;
                var bvWarehouseEmail = WebAppSettings.ContactEmail;
                if (bvPackage.Items.Count > 0)
                {
                    var bvItem = LineItem.FindByBvin(bvPackage.Items[0].LineItemBvin);
                    if (bvItem != null && bvItem.Bvin.Equals(bvPackage.Items[0].LineItemBvin, StringComparison.InvariantCultureIgnoreCase))
                    {
                        if (bvItem.AssociatedProduct != null)
                        {
                            if (bvItem.AssociatedProduct.ShippingMode == ShippingMode.ShipFromManufacturer)
                            {
                                var bvManufacturer = Manufacturer.FindByBvin(bvItem.AssociatedProduct.ManufacturerId);
                                if (bvManufacturer != null &&
                                    bvManufacturer.Bvin.Equals(bvItem.AssociatedProduct.ManufacturerId, StringComparison.InvariantCultureIgnoreCase))
                                {
                                    bvWarehouse = bvManufacturer.Address;
                                    bvWarehouseEmail = bvManufacturer.EmailAddress;
                                }
                            }
                            if (bvItem.AssociatedProduct.ShippingMode == ShippingMode.ShipFromVendor)
                            {
                                var bvVendor = Vendor.FindByBvin(bvItem.AssociatedProduct.VendorId);
                                if (bvVendor != null &&
                                    bvVendor.Bvin.Equals(bvItem.AssociatedProduct.VendorId, StringComparison.InvariantCultureIgnoreCase))
                                {
                                    bvWarehouse = bvVendor.Address;
                                    bvWarehouseEmail = bvVendor.EmailAddress;
                                }
                            }
                        }
                    }
                }
                package.Warehouse = BuildAddress(bvWarehouse, bvWarehouseEmail);
                package.Weight = GetWeightInPounds(bvPackage.Weight, bvPackage.WeightUnits);
                package.Width = GetDimensionInInches(bvPackage.Width, bvPackage.SizeUnits);
                return new PackageResult { Success = true, Package = package };
            }
            catch (ApplicationException ex)
            {
                return new PackageResult { Success = false, Message = ex.Message, Package = package };
            }
            catch (Exception ex)
            {
                return new PackageResult { Success = false, Message = ex.ToString(), Package = package };
            }
        }

        private static NamedAmount[] BuildPayments(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            var payments = new NamedAmountList();
            foreach (var bvPayment in bvOrder.Payments)
            {
                if (bvPayment.AmountCharged == 0) continue;
                payments[bvPayment.PaymentMethodName, bvPayment.FriendlyStatus].Amount += bvPayment.AmountCharged;
            }
            return payments.Count > 0 ? payments.ToArray() : null;
        }

        private static ShippingAddress BuildShippingAddress(BVSoftware.Bvc5.Core.Contacts.Address bvAddress, string email)
        {
            if (bvAddress == null) return null;

            var address = new ShippingAddress();
            address.AddressId = string.IsNullOrEmpty(bvAddress.Bvin) ? GetUniqueAddressId(bvAddress) : bvAddress.Bvin;
            address.City = ShippingAgentHelper.StringOrNull(bvAddress.City);
            address.Company = ShippingAgentHelper.StringOrNull(bvAddress.Company);
            address.Country = ShippingAgentHelper.StringOrNull(bvAddress.CountryName);
            if (!string.IsNullOrEmpty(bvAddress.CountryBvin))
            {
                var country = Country.FindByBvin(bvAddress.CountryBvin);
                if (country != null && country.Bvin.Equals(bvAddress.CountryBvin, StringComparison.InvariantCultureIgnoreCase))
                    address.Country = country.IsoCode;
            }
            address.Email = email;
            address.Fax = ShippingAgentHelper.StringOrNull(bvAddress.Fax);
            address.FirstName = ShippingAgentHelper.StringOrNull(bvAddress.FirstName);
            address.LastName = ShippingAgentHelper.StringOrNull(bvAddress.LastName);
            address.Line1 = ShippingAgentHelper.StringOrNull(bvAddress.Line1);
            address.Line2 = ShippingAgentHelper.StringOrNull(bvAddress.Line2);
            address.Phone = ShippingAgentHelper.StringOrNull(bvAddress.Phone);
            address.PostalCode = ShippingAgentHelper.StringOrNull(bvAddress.PostalCode);
            address.Region = ShippingAgentHelper.StringOrNull(bvAddress.RegionName);
            address.Residential = bvAddress.Residential;

            return address;
        }

        private static string BuildShippingStatus(OrderShippingStatus orderShippingStatus)
        {
            switch (orderShippingStatus)
            {
                case OrderShippingStatus.FullyShipped:
                    return "Fully Shipped";
                case OrderShippingStatus.NonShipping:
                    return "Non Shipping";
                case OrderShippingStatus.PartiallyShipped:
                    return "Partially Shipped";
                case OrderShippingStatus.Unshipped:
                    return "Unshipped";
                default:
                    return string.Empty;
            }
        }

        private static UpdateResult BuildUpdateResult(Update update, BVSoftware.Bvc5.Core.Shipping.Package bvPackage)
        {
            var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvPackage.OrderId);
            if (bvOrder == null || !bvOrder.Bvin.Equals(bvPackage.OrderId, StringComparison.InvariantCultureIgnoreCase))
                return new UpdateResult { Success = false, Message = "Invalid order ID." };

            var notes = new List<string>();

            if (update.Destination != null)
            {
                var bvCountry = GetCountryByName(update.Destination.Country);
                var bvRegion = GetRegionByName(bvCountry.Bvin, update.Destination.Region);

                if (!AddressesAreEqual(bvOrder.ShippingAddress, update.Destination, bvCountry.Bvin, bvRegion.Bvin))
                {
                    bvOrder.ShippingAddress.City = update.Destination.City;
                    bvOrder.ShippingAddress.Company = update.Destination.Company;
                    bvOrder.ShippingAddress.CountryBvin = bvCountry.Bvin;
                    bvOrder.ShippingAddress.CountryName = bvCountry.IsoCode;
                    bvOrder.ShippingAddress.Fax = update.Destination.Fax;
                    bvOrder.ShippingAddress.FirstName = update.Destination.FirstName;
                    bvOrder.ShippingAddress.LastName = update.Destination.LastName;
                    if (bvOrder.ShippingAddress.Line1 != update.Destination.Line1
                        || bvOrder.ShippingAddress.Line2 != update.Destination.Line2)
                    {
                        bvOrder.ShippingAddress.Line1 = update.Destination.Line1;
                        bvOrder.ShippingAddress.Line2 = update.Destination.Line2;
                        bvOrder.ShippingAddress.Line3 = string.Empty;
                    }
                    bvOrder.ShippingAddress.Phone = update.Destination.Phone;
                    bvOrder.ShippingAddress.PostalCode = update.Destination.PostalCode;
                    bvOrder.ShippingAddress.RegionBvin = bvRegion.Bvin;
                    bvOrder.ShippingAddress.RegionName = bvRegion.Name;
                    bvOrder.ShippingAddress.Residential = update.Destination.Residential;

                    if (BVSoftware.Bvc5.Core.Orders.Order.Update(bvOrder))
                        notes.Add("Updated order shipping address.");
                    else
                        return new UpdateResult { Success = false, Message = "Cannot update order shipping address." };
                }
            }

            if (update.TrackingNumber != null)
            {
                if (!bvPackage.TrackingNumber.Equals(update.TrackingNumber, StringComparison.InvariantCultureIgnoreCase))
                {
                    bvPackage.TrackingNumber = update.TrackingNumber;
                    if (BVSoftware.Bvc5.Core.Shipping.Package.Update(bvPackage))
                        notes.Add(string.Format("Changed package tracking number to {0}.", update.TrackingNumber));
                    else
                        return new UpdateResult { Success = false, Message = "Cannot update package tracking number." };
                }
            }

            if (update.TrackingProviderId != null)
            {
                if (!bvPackage.ShippingProviderId.Equals(update.TrackingProviderId, StringComparison.InvariantCultureIgnoreCase))
                {
                    bvPackage.ShippingProviderId = update.TrackingProviderId;
                    if (BVSoftware.Bvc5.Core.Shipping.Package.Update(bvPackage))
                    {
                        var bvProvider = AvailableProviders.FindProviderById(update.TrackingProviderId);
                        notes.Add(string.Format("Changed package tracking provider to {0}.", bvProvider.Name));
                    }
                    else
                        return new UpdateResult { Success = false, Message = "Cannot update package tracking provider." };
                }
            }

            if (update.ShipDateSpecified)
            {
                if (bvPackage.ShipDate != update.ShipDate)
                {
                    bvPackage.ShipDate = update.ShipDate;
                    if (BVSoftware.Bvc5.Core.Shipping.Package.Update(bvPackage))
                        notes.Add(string.Format("Changed package ship date {0}.", update.ShipDate));
                    else
                        return new UpdateResult { Success = false, Message = "Cannot update package ship date." };
                }
                if (!bvPackage.HasShipped)
                {
                    if (bvPackage.Ship())
                        notes.Add("Changed package to shipped.");
                    else
                        return new UpdateResult { Success = false, Message = "Cannot ship package." };
                }
            }

            if (update.ShippingCostSpecified)
            {
                notes.Add(string.Format("Actual shipping cost is {0:c}.", update.ShippingCost));
            }

            if (notes.Count > 0)
            {
                var message = string.Join(" ", notes.ToArray());
                var text = string.Format(ShippingAgentHelper.ORDER_UPDATE_TEXT, message);
                bvOrder.AddNote(new OrderNote { Note = text, NoteType = OrderNoteType.System });

                // Run the "Shipping Changed" workflow using a fresh copy of the order.
                var bvContext = new OrderTaskContext();
                bvContext.Order = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvPackage.OrderId);
                bvContext.UserId = bvOrder.UserID;
                Workflow.RunByBvin(bvContext, WebAppSettings.WorkflowIdShippingChanged);

                bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvPackage.OrderId);

                var result = new UpdateResult();
                result.Message = message;
                result.OrderResult = BuildOrderResult(bvOrder);
                result.PackageResult = BuildPackageResult(bvPackage);
                result.Success = true;
                return result;
            }

            return new UpdateResult { Success = true, Message = "No change." };
        }

        private static Country GetCountryByName(string name)
        {
            var bvCountry = Country.FindByISOCode(name);
            if (bvCountry != null && bvCountry.IsoCode.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                return bvCountry;

            foreach (var country in Country.FindAll())
            {
                if (country.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                    return country;
            }

            return new Country { Bvin = string.Empty, DisplayName = name };
        }

        private static double GetDimensionInInches(decimal dimension, LengthType lengthType)
        {
            return Convert.ToDouble(lengthType == LengthType.Inches ? dimension : dimension * 0.393700787m);
        }

        /// <summary>
        /// Return one shipment from the store.
        /// </summary>
        /// <param name="orderId">Ignored by this implementation.</param>
        /// <param name="packageId">The BVC5 Package bvin.</param>
        /// <returns>The Shipment that corresponds to the BVC5 Package.</returns>
        protected static PackageResult GetPackageResult(string orderId, string packageId)
        {
            var bvPackage = BVSoftware.Bvc5.Core.Shipping.Package.FindByBvin(packageId);
            if (bvPackage == null || !bvPackage.Bvin.Equals(packageId, StringComparison.InvariantCultureIgnoreCase))
                throw new ApplicationException("Invalid package ID.");

            return BuildPackageResult(bvPackage);
        }

        private static IEnumerable<BVSoftware.Bvc5.Core.Shipping.Package> GetPackages(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            PackageList packages = new PackageList();
            packages.AddRange(bvOrder.FindPackages());

            // Create suggested packages for items that are not already in other packages
            Collection<ShippingGroup> groups;
            try
            {
                groups = bvOrder.GetShippingGroups();
            }
            catch
            {
                throw new ApplicationException("Order " + bvOrder.OrderNumber +
                                               " contains at least 1 deleted product or product variation. Use BV Admin to remove the corresponding item from the order.");
            }

            foreach (var bvGroup in groups)
            {
                var bvPackage = new BVSoftware.Bvc5.Core.Shipping.Package();
                foreach (var bvItem in bvGroup.Items)
                {
                    var expectedQuantity = bvItem.Quantity - bvItem.QuantityShipped;
                    var totalQuantity = packages.GetTotalQuantity(bvItem.Bvin);
                    if (totalQuantity < expectedQuantity)
                        bvPackage.Items.Add(new PackageItem(bvItem.ProductId, bvItem.Bvin, expectedQuantity - totalQuantity));
                }

                if (bvPackage.Items.Count <= 0)
                    continue;

                bvGroup.GenerateDimensions();
                bvPackage.Bvin = Guid.NewGuid().ToString();
                bvPackage.CustomProperties.Add("Structured Solutions", "CreatedBy", "Shipping Agent");
                bvPackage.Description = "This package was created by Shipping Agent.";
                bvPackage.HasShipped = false;
                bvPackage.Height = bvGroup.Height;
                bvPackage.Length = bvGroup.Length;
                bvPackage.OrderId = bvOrder.Bvin;
                //BVC5SP3 bvPackage.ShippingMethodId is not stored in the database
                bvPackage.ShippingProviderId = bvOrder.ShippingProviderId;
                bvPackage.ShippingProviderServiceCode = bvOrder.ShippingProviderServiceCode;
                bvPackage.SizeUnits = WebAppSettings.SiteShippingLengthType;
                bvPackage.Weight = bvGroup.Weight;
                bvPackage.WeightUnits = WebAppSettings.SiteShippingWeightType;
                bvPackage.Width = bvGroup.Width;

                if (!bvOrder.AddPackage(bvPackage))
                    continue;

                packages.Add(bvPackage);
            }
            return packages;
        }

        protected static List<RateProvider> GetRateProvidersFromStore()
        {
            var dictionary = new Dictionary<string, List<string>>();
            foreach (var bvMethod in ShippingMethod.FindAll())
            {
                var bvProvider = AvailableProviders.FindProviderById(bvMethod.ShippingProviderId);
                if (bvProvider == null || bvProvider.ProviderId.Equals(NULL_PROVIDER_ID, StringComparison.InvariantCultureIgnoreCase))
                    continue;

                if (!dictionary.ContainsKey(bvProvider.Name))
                {
                    dictionary.Add(bvProvider.Name, new List<string>());
                }

                var rateOptions = dictionary[bvProvider.Name];
                if (!rateOptions.Contains(bvMethod.Name))
                {
                    rateOptions.Add(bvMethod.Name);
                }
            }

            var rateProviders = new List<RateProvider>();
            foreach (var pair in dictionary)
            {
                var rateProvider = new RateProvider();
                rateProvider.Name = pair.Key;
                rateProvider.RateOptions = pair.Value.ToArray();
                rateProviders.Add(rateProvider);
            }

            return rateProviders;
        }

        private static Region GetRegionByName(string countryBvin, string name)
        {
            if (!string.IsNullOrEmpty(countryBvin))
            {
                foreach (var region in Region.FindByCountry(countryBvin))
                {
                    if (region.Abbreviation.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                        return region;
                    if (region.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                        return region;
                }
            }
            return new Region { Bvin = string.Empty, Name = name };
        }

        private static string GetUniqueAddressId(BVSoftware.Bvc5.Core.Contacts.Address bvAddress)
        {
            using (var ms = new MemoryStream())
            using (var writer = new StreamWriter(ms))
            {
                writer.Write(bvAddress.FirstName);
                writer.Write(bvAddress.LastName);
                writer.Write(bvAddress.Company);
                writer.Write(bvAddress.Line1);
                writer.Write(bvAddress.Line2);
                writer.Write(bvAddress.City);
                writer.Write(bvAddress.RegionName);
                writer.Write(bvAddress.CountryName);
                writer.Write(bvAddress.PostalCode);
                writer.Write(bvAddress.Phone);
                writer.Write(bvAddress.Fax);
                writer.Flush();
                ms.Seek(0, SeekOrigin.Begin);
                using (var crypto = new MD5CryptoServiceProvider())
                {
                    return Convert.ToBase64String(crypto.ComputeHash(ms));
                }
            }
        }

        private static double GetWeightInPounds(decimal weight, WeightType weightType)
        {
            return Convert.ToDouble(weightType == WeightType.Pounds ? weight : weight * 2.20462262M);
        }
    }
}
